using System;
using System.Globalization;
using System.Reflection;
using System.Reflection.Emit;
using Microsoft.Practices.ObjectBuilder2;

namespace UniAspect.Aspect.Emit
{
    /// <summary>
    /// Replaces default Unity builder with proxy generator
    /// </summary>
    [Obsolete]
    public class AspectInsertionStrategy : BuilderStrategy
    {
        private static readonly MethodInfo throwForNullExistingObject =
                typeof(DynamicMethodConstructorStrategy)
                        .GetMethod("ThrowForNullExistingObject");

        private static readonly MethodInfo throwForResolutionFailed =
                typeof(DynamicMethodConstructorStrategy)
                        .GetMethod("ThrowForResolutionFailed");

        private static readonly MethodInfo throwForAttemptingToConstructInterface =
                typeof(DynamicMethodConstructorStrategy)
                        .GetMethod("ThrowForAttemptingToConstructInterface");

        private static readonly MethodInfo GetPoliciesFromContext =
                typeof(IBuilderContext).GetProperty("Policies").GetGetMethod();

        private static readonly MethodInfo GetBuildKeyFromContext =
                typeof(IBuilderContext).GetProperty("BuildKey").GetGetMethod();

        private static readonly MethodInfo GetPolicy =
                typeof(IPolicyList).GetMethod("Get", Types(typeof(Type), typeof(object)));

        private static readonly MethodInfo GetTypeFromHandle =
                typeof(Type).GetMethod("GetTypeFromHandle", Types(typeof(RuntimeTypeHandle)));

        private static readonly MethodInfo CreateClassProxyMethod =
                typeof(IAspectInsertionPolicy)
                        .GetMethod("CreateClassProxy", Types(typeof(Type), typeof(IBuilderContext), typeof(object[])));

        private static readonly MethodInfo CreateInterfaceProxyMethod =
                typeof(IAspectInsertionPolicy)
                        .GetMethod("CreateInterfaceProxy", Types(typeof(object), typeof(object)));

        private static readonly MethodInfo NoAspectsProperty =
                typeof(IAspectInsertionPolicy).GetProperty("NoAspects").GetGetMethod();


        /// <summary>
        /// Called during the chain of responsibility for a build operation.
        /// </summary>
        /// <remarks>Existing object is an instance of <see cref="DynamicBuildPlanGenerationContext"/>.</remarks>
        /// <param name="context">The context for the operation.</param>
        public override void PreBuildUp(IBuilderContext context)
        {
            Guard.ArgumentNotNull(context, "context");

            var buildContext = (DynamicBuildPlanGenerationContext)context.Existing;

            var selector = context.Policies.Get<IConstructorSelectorPolicy>(context.BuildKey);

            SelectedConstructor selectedCtor = selector.SelectConstructor(context);
            // Method preamble - test if we have an existing object
            // First off, set up jump - if there's an existing object, skip us entirely
            Label existingObjectNotNull = buildContext.IL.DefineLabel();
            buildContext.EmitLoadExisting();
            buildContext.IL.Emit(OpCodes.Ldnull);
            buildContext.IL.Emit(OpCodes.Ceq);
            buildContext.IL.Emit(OpCodes.Brfalse, existingObjectNotNull);

            // Verify we're not attempting to create an instance of an interface
            buildContext.EmitLoadContext();
            buildContext.IL.EmitCall(OpCodes.Call, throwForAttemptingToConstructInterface, null);

            if(selectedCtor != null)
            {
                buildContext.IL.BeginExceptionBlock();

                EmitCreate(buildContext, selectedCtor);

                string signatureString = CreateSignatureString(selectedCtor.Constructor);

                // now proxy is created and it is in the stack

                buildContext.EmitStoreExisting();

                buildContext.IL.BeginCatchBlock(typeof(Exception));

                Label exceptionOccuredInResolution = buildContext.IL.DefineLabel();
                LocalBuilder currentParameterName = buildContext.IL.DeclareLocal(typeof(string));

                buildContext.IL.Emit(OpCodes.Ldnull);
                buildContext.IL.Emit(OpCodes.Stloc, currentParameterName);
                buildContext.IL.Emit(OpCodes.Ldloc, currentParameterName);
                buildContext.IL.Emit(OpCodes.Ldnull);
                buildContext.IL.Emit(OpCodes.Ceq);
                buildContext.IL.Emit(OpCodes.Brfalse, exceptionOccuredInResolution);
                buildContext.IL.Emit(OpCodes.Rethrow);

                buildContext.IL.MarkLabel(exceptionOccuredInResolution);
                buildContext.IL.Emit(OpCodes.Ldloc, currentParameterName);
                buildContext.IL.Emit(OpCodes.Ldstr, signatureString);
                buildContext.EmitLoadContext();
                buildContext.IL.EmitCall(OpCodes.Call, throwForResolutionFailed, null);

                buildContext.IL.EndExceptionBlock();
            }
            else
            {
                // If we get here, object has no constructors. It's either
                // an interface or a primitive (like int). In this case,
                // verify that we have an Existing object, and if not,
                // throw (via helper function).
                buildContext.EmitLoadContext();
                buildContext.IL.EmitCall(OpCodes.Call, throwForNullExistingObject, null);
            }

            buildContext.IL.MarkLabel(existingObjectNotNull);
        }

        private void EmitCreate(DynamicBuildPlanGenerationContext buildContext,
                                SelectedConstructor selectedCtor)
        {
            LocalBuilder aspectPolicy = buildContext.IL.DeclareLocal(typeof(IAspectInsertionPolicy));

            EmitGetPolicy(buildContext, typeof(IAspectInsertionPolicy));
            buildContext.IL.Emit(OpCodes.Stloc, aspectPolicy);

            buildContext.IL.Emit(OpCodes.Ldloc, aspectPolicy);
            buildContext.IL.EmitCall(OpCodes.Callvirt, NoAspectsProperty, null);

            var gotoCreateConstructor = buildContext.IL.DefineLabel();
            var gotoEnd = buildContext.IL.DefineLabel();
            buildContext.IL.Emit(OpCodes.Brtrue, gotoCreateConstructor);

            EmitCreateProxy(buildContext, aspectPolicy, selectedCtor);

            buildContext.IL.Emit(OpCodes.Br, gotoEnd);

            buildContext.IL.MarkLabel(gotoCreateConstructor);

            EmitCreateWithConstructor(buildContext, selectedCtor);

            buildContext.IL.MarkLabel(gotoEnd);
        }

        private void EmitCreateProxy(DynamicBuildPlanGenerationContext buildContext, LocalBuilder aspectPolicy, SelectedConstructor selectedCtor)
        {
// Resolve parameters
            ParameterInfo[] parameters = selectedCtor.Constructor.GetParameters();
            var constructorParameterKeys = selectedCtor.GetParameterKeys();
            var constructorParameters = buildContext.IL.DeclareLocal(typeof(object[]));

            //creating array for parameters
            buildContext.IL.Emit(OpCodes.Ldc_I4, constructorParameterKeys.Length);
            buildContext.IL.Emit(OpCodes.Newarr, typeof(object));
            //storing array to the local variable
            buildContext.IL.Emit(OpCodes.Stloc, constructorParameters);

            LocalBuilder currentParameterName = buildContext.IL.DeclareLocal(typeof(string));

            int i = 0;
            foreach(string parameterKey in constructorParameterKeys)
            {
                buildContext.IL.Emit(OpCodes.Ldloc, constructorParameters);
                buildContext.IL.Emit(OpCodes.Ldc_I4, i);

                buildContext.IL.Emit(OpCodes.Ldstr, parameters[i].Name);
                buildContext.IL.Emit(OpCodes.Stloc, currentParameterName);
                buildContext.EmitResolveDependency(parameters[i].ParameterType, parameterKey);

                buildContext.IL.Emit(OpCodes.Stelem_Ref);
                i++;
            }

            //clear parameter name variable
            buildContext.IL.Emit(OpCodes.Ldnull);
            buildContext.IL.Emit(OpCodes.Stloc, currentParameterName);

            buildContext.IL.Emit(OpCodes.Ldloc, aspectPolicy);

            //setting type that will be created
            buildContext.IL.Emit(OpCodes.Ldtoken, selectedCtor.Constructor.DeclaringType);
            buildContext.IL.EmitCall(OpCodes.Call, GetTypeFromHandle, null);

            //builder context - pushed to the stack
            buildContext.IL.Emit(OpCodes.Ldarg_0);
                
            //loading parameters for create proxy policy call
            buildContext.IL.Emit(OpCodes.Ldloc, constructorParameters);

            //creating proxy, put it on top of a stack
            buildContext.IL.EmitCall(OpCodes.Callvirt, CreateClassProxyMethod, null);

            //clear constructorParameters variable
            buildContext.IL.Emit(OpCodes.Ldnull);
            buildContext.IL.Emit(OpCodes.Stloc, constructorParameters);
        }

        private void EmitCreateWithConstructor(DynamicBuildPlanGenerationContext buildContext,
                                               SelectedConstructor selectedCtor)
        {
            LocalBuilder currentParameterName = buildContext.IL.DeclareLocal(typeof(string));

            // Resolve parameters
            ParameterInfo[] parameters = selectedCtor.Constructor.GetParameters();

            int i = 0;
            foreach(string parameterKey in selectedCtor.GetParameterKeys())
            {
                buildContext.IL.Emit(OpCodes.Ldstr, parameters[i].Name);
                buildContext.IL.Emit(OpCodes.Stloc, currentParameterName);
                buildContext.EmitResolveDependency(parameters[i].ParameterType, parameterKey);
                i++;
            }

            // Call the constructor

            buildContext.IL.Emit(OpCodes.Ldnull);
            buildContext.IL.Emit(OpCodes.Stloc, currentParameterName);

            buildContext.IL.Emit(OpCodes.Newobj, selectedCtor.Constructor);
        }

        /// <summary>
        /// Ends up with having policy in the stack
        /// </summary>
        /// <param name="buildContext"></param>
        /// <param name="policyType"></param>
        private void EmitGetPolicy(DynamicBuildPlanGenerationContext buildContext, Type policyType)
        {
            buildContext.IL.Emit(OpCodes.Ldarg_0);
            buildContext.IL.EmitCall(OpCodes.Callvirt, GetPoliciesFromContext, null);

            //push type to the stack to make a call
            buildContext.IL.Emit(OpCodes.Ldtoken, policyType);
            buildContext.IL.EmitCall(OpCodes.Call, GetTypeFromHandle, null);

            //key for policy - pushed to the stack
            buildContext.IL.Emit(OpCodes.Ldarg_0);
            buildContext.IL.EmitCall(OpCodes.Callvirt, GetBuildKeyFromContext, null);

            //getting policy
            buildContext.IL.EmitCall(OpCodes.Callvirt, GetPolicy, null);

            buildContext.IL.Emit(OpCodes.Castclass, policyType);
        }

        [Obsolete("There seems to be no case when this can be used correctly.")]
        private void EmitCreateInterfaceProxy(DynamicBuildPlanGenerationContext buildContext, SelectedConstructor selectedCtor)
        {
            LocalBuilder currentParameterName = buildContext.IL.DeclareLocal(typeof(string));

            // Resolve parameters
            ParameterInfo[] parameters = selectedCtor.Constructor.GetParameters();

            buildContext.IL.BeginExceptionBlock();

            //gettting policies list
            buildContext.IL.Emit(OpCodes.Ldarg_0);
            buildContext.IL.EmitCall(OpCodes.Callvirt, GetPoliciesFromContext, null);

            //push type to the stack to make a call
            buildContext.IL.Emit(OpCodes.Ldtoken, typeof(IAspectInsertionPolicy));
            buildContext.IL.EmitCall(OpCodes.Call, GetTypeFromHandle, null);

            //key for policy - pushed to the stack
            buildContext.IL.Emit(OpCodes.Ldarg_0);
            buildContext.IL.EmitCall(OpCodes.Callvirt, GetBuildKeyFromContext, null);

            //getting policy
            buildContext.IL.EmitCall(OpCodes.Callvirt, GetPolicy, null);
            buildContext.IL.Emit(OpCodes.Castclass, typeof(IAspectInsertionPolicy));

            int i = 0;
            foreach(string parameterKey in selectedCtor.GetParameterKeys())
            {
                buildContext.IL.Emit(OpCodes.Ldstr, parameters[i].Name);
                buildContext.IL.Emit(OpCodes.Stloc, currentParameterName);
                buildContext.EmitResolveDependency(parameters[i].ParameterType, parameterKey);
                ++i;
            }

            // Call the constructor

            buildContext.IL.Emit(OpCodes.Ldnull);
            buildContext.IL.Emit(OpCodes.Stloc, currentParameterName);

            buildContext.IL.Emit(OpCodes.Newobj, selectedCtor.Constructor);

            //key for policy - pushed to the stack
            buildContext.IL.Emit(OpCodes.Ldarg_0);
            buildContext.IL.EmitCall(OpCodes.Callvirt, GetBuildKeyFromContext, null);

            buildContext.IL.EmitCall(OpCodes.Callvirt, CreateInterfaceProxyMethod, null);
        }

        // Build up the string that will represent the constructor signature
        // in any exception message.
        private static string CreateSignatureString(ConstructorInfo ctor)
        {
            string typeName = ctor.DeclaringType.FullName;
            ParameterInfo[] parameters = ctor.GetParameters();
            var parameterDescriptions = new string[parameters.Length];
            for(int i = 0; i < parameters.Length; ++i)
            {
                parameterDescriptions[i] = string.Format(CultureInfo.CurrentCulture,
                                                         "{0} {1}",
                                                         parameters[i].ParameterType.FullName,
                                                         parameters[i].Name);
            }

            return string.Format(CultureInfo.CurrentCulture,
                                 "{0}({1})",
                                 typeName,
                                 string.Join(", ", parameterDescriptions));
        }

        private static Type[] Types(params Type[] types)
        {
            return types;
        }
    }
}